Java 26일 코스 - Day 16: 컬렉션 Set, Map

Day 16: 컬렉션 Set, Map

Set은 중복을 허용하지 않는 컬렉션이고, Map은 키-값 쌍으로 데이터를 저장하는 컬렉션입니다. Set은 주머니에 구슬을 넣되 같은 색 구슬은 하나만 보관하는 것, Map은 사전처럼 단어(키)로 뜻(값)을 찾는 것과 같습니다.

HashSet과 TreeSet

중복 없는 집합을 다루는 두 가지 구현체입니다.

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;

public class SetExample {
    public static void main(String[] args) {
        // HashSet: 순서 보장 안 됨, O(1) 접근
        Set<String> hashSet = new HashSet<>();
        hashSet.add("Java");
        hashSet.add("Python");
        hashSet.add("Go");
        hashSet.add("Java");  // 중복! 추가되지 않음
        hashSet.add("Rust");

        System.out.println("HashSet: " + hashSet);       // 순서 불규칙
        System.out.println("크기: " + hashSet.size());    // 4 (중복 제외)
        System.out.println("Java 포함? " + hashSet.contains("Java")); // true

        // LinkedHashSet: 삽입 순서 유지
        Set<String> linkedSet = new LinkedHashSet<>();
        linkedSet.add("첫 번째");
        linkedSet.add("두 번째");
        linkedSet.add("세 번째");
        System.out.println("LinkedHashSet: " + linkedSet); // 삽입 순서 유지

        // TreeSet: 자동 정렬 (오름차순)
        Set<Integer> treeSet = new TreeSet<>();
        treeSet.add(42);
        treeSet.add(15);
        treeSet.add(8);
        treeSet.add(99);
        treeSet.add(23);
        System.out.println("TreeSet: " + treeSet); // [8, 15, 23, 42, 99]

        // 집합 연산
        Set<String> setA = new HashSet<>(Set.of("Java", "Python", "Go"));
        Set<String> setB = new HashSet<>(Set.of("Python", "Rust", "Go"));

        // 합집합
        Set<String> union = new HashSet<>(setA);
        union.addAll(setB);
        System.out.println("합집합: " + union);

        // 교집합
        Set<String> intersection = new HashSet<>(setA);
        intersection.retainAll(setB);
        System.out.println("교집합: " + intersection);

        // 차집합
        Set<String> difference = new HashSet<>(setA);
        difference.removeAll(setB);
        System.out.println("차집합: " + difference);
    }
}

HashMap 기본 사용법

키-값 쌍을 저장하는 가장 많이 사용되는 Map 구현체입니다.

import java.util.HashMap;
import java.util.Map;

public class HashMapBasic {
    public static void main(String[] args) {
        Map<String, Integer> scores = new HashMap<>();

        // 추가
        scores.put("홍길동", 85);
        scores.put("김영희", 92);
        scores.put("이철수", 78);
        scores.put("박지민", 95);

        // 조회
        System.out.println("홍길동 점수: " + scores.get("홍길동"));      // 85
        System.out.println("없는 키: " + scores.get("최수진"));          // null
        System.out.println("기본값: " + scores.getOrDefault("최수진", 0)); // 0

        // 수정 (같은 키로 put하면 값 덮어쓰기)
        scores.put("홍길동", 90);
        System.out.println("수정 후: " + scores.get("홍길동")); // 90

        // 포함 여부
        System.out.println("키 포함? " + scores.containsKey("김영희"));    // true
        System.out.println("값 포함? " + scores.containsValue(78));       // true

        // 삭제
        scores.remove("이철수");
        System.out.println("크기: " + scores.size()); // 3

        // 순회 방법들
        // 1. entrySet (키-값 모두 필요할 때 가장 효율적)
        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // 2. keySet
        for (String name : scores.keySet()) {
            System.out.println(name + " -> " + scores.get(name));
        }

        // 3. forEach + 람다
        scores.forEach((name, score) ->
            System.out.println(name + " = " + score));
    }
}

Map 고급 활용

Java 8 이후 추가된 편리한 메서드들을 알아봅니다.

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class MapAdvanced {
    public static void main(String[] args) {
        // 단어 빈도수 세기
        String text = "Java는 객체지향 언어이다 Java는 플랫폼 독립적이다 Java는 안전하다";
        String[] words = text.split(" ");

        Map<String, Integer> wordCount = new HashMap<>();
        for (String word : words) {
            // merge: 키가 있으면 병합, 없으면 추가
            wordCount.merge(word, 1, Integer::sum);
        }
        System.out.println("단어 빈도: " + wordCount);

        // putIfAbsent: 키가 없을 때만 추가
        Map<String, String> config = new HashMap<>();
        config.put("host", "localhost");
        config.putIfAbsent("host", "remote-server"); // 이미 있으므로 무시
        config.putIfAbsent("port", "8080");          // 없으므로 추가
        System.out.println("설정: " + config);

        // computeIfAbsent: 캐시 패턴
        Map<Integer, String> cache = new HashMap<>();
        String result = cache.computeIfAbsent(42, key -> {
            System.out.println("비용이 큰 계산 수행 중...");
            return "결과-" + key;
        });
        System.out.println("첫 호출: " + result);

        // 두 번째 호출: 이미 캐시에 있으므로 계산하지 않음
        String cached = cache.computeIfAbsent(42, key -> "새 계산");
        System.out.println("캐시 히트: " + cached);

        // TreeMap: 키 기준 자동 정렬
        Map<String, Integer> treeMap = new TreeMap<>();
        treeMap.put("banana", 3);
        treeMap.put("apple", 5);
        treeMap.put("cherry", 1);
        System.out.println("TreeMap: " + treeMap); // 알파벳순

        // 불변 Map 생성
        Map<String, Integer> immutable = Map.of(
            "one", 1,
            "two", 2,
            "three", 3
        );
        System.out.println("불변 Map: " + immutable);
    }
}

실전 예제: 전화번호부

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PhoneBook {
    private final Map<String, List<String>> contacts = new HashMap<>();

    void addContact(String name, String phone) {
        contacts.computeIfAbsent(name, k -> new ArrayList<>()).add(phone);
        System.out.println(name + "의 번호 " + phone + " 추가됨");
    }

    List<String> findByName(String name) {
        return contacts.getOrDefault(name, List.of());
    }

    void removeContact(String name) {
        if (contacts.remove(name) != null) {
            System.out.println(name + " 삭제됨");
        } else {
            System.out.println(name + "을(를) 찾을 수 없음");
        }
    }

    void printAll() {
        System.out.println("=== 전화번호부 ===");
        contacts.forEach((name, phones) -> {
            System.out.println(name + ": " + String.join(", ", phones));
        });
        System.out.println("총 " + contacts.size() + "명의 연락처");
    }

    Map<String, Integer> getStatistics() {
        Map<String, Integer> stats = new HashMap<>();
        stats.put("총 연락처 수", contacts.size());
        stats.put("총 전화번호 수", contacts.values().stream()
                .mapToInt(List::size).sum());
        return stats;
    }

    public static void main(String[] args) {
        PhoneBook phoneBook = new PhoneBook();

        phoneBook.addContact("홍길동", "010-1234-5678");
        phoneBook.addContact("홍길동", "02-999-0000");  // 같은 이름, 다른 번호
        phoneBook.addContact("김영희", "010-9876-5432");
        phoneBook.addContact("이철수", "010-5555-3333");

        phoneBook.printAll();

        System.out.println("\n홍길동 번호: " + phoneBook.findByName("홍길동"));
        System.out.println("통계: " + phoneBook.getStatistics());
    }
}

오늘의 연습문제

  1. 중복 문자 찾기: 문자열에서 중복되는 문자와 중복되지 않는 문자를 각각 Set으로 분류하여 출력하세요. 입력: "programming", 출력: 중복={g, r, m}, 유일={p, o, a, i, n}.

  2. 학생 그룹핑: Map<String, List<Student>>를 사용하여 학생들을 학점별로 그룹핑하세요. A학점 학생 목록, B학점 학생 목록 등으로 출력하세요.

  3. 영한 사전: TreeMap을 사용한 영한 사전을 구현하세요. 단어 추가, 검색, 특정 알파벳으로 시작하는 단어 목록 조회(subMap 활용), 전체 단어 알파벳순 출력 기능을 만드세요.

이 글이 도움이 되었나요?